/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.wfs.xml;

import static org.geoserver.ows.util.ResponseUtils.buildURL;
import static org.geoserver.ows.util.ResponseUtils.params;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.eclipse.xsd.XSDComplexTypeDefinition;
import org.eclipse.xsd.XSDCompositor;
import org.eclipse.xsd.XSDDerivationMethod;
import org.eclipse.xsd.XSDElementDeclaration;
import org.eclipse.xsd.XSDFactory;
import org.eclipse.xsd.XSDForm;
import org.eclipse.xsd.XSDImport;
import org.eclipse.xsd.XSDModelGroup;
import org.eclipse.xsd.XSDParticle;
import org.eclipse.xsd.XSDSchema;
import org.eclipse.xsd.XSDSchemaContent;
import org.eclipse.xsd.XSDTypeDefinition;
import org.eclipse.xsd.util.XSDConstants;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.ResponseUtils;
import org.geotools.gml2.GML;
import org.geotools.util.logging.Logging;
import org.geotools.wfs.WFS;
import org.geotools.xml.SchemaIndex;
import org.geotools.xml.Schemas;
import org.geotools.xml.XSD;
import org.geotools.xml.impl.SchemaIndexImpl;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.feature.type.Schema;

/**
 * XSD for an application schema of a geoserver feature type.
 * 
 * @author Justin Deoliveira, OpenGeo
 *
 */
public class ApplicationSchemaXSD extends XSD {

    static Logger LOGGER = Logging.getLogger( "org.geoserver.wfs" );
    
    /**
     * namespace for the app schema
     */
    NamespaceInfo ns;
    
    /**
     * the catalog
     */
    Catalog catalog;
    /**
     * base url used for schemaLocation
     */
    String baseURL;
    
    /**
     * wfs schema
     */
    WFS wfs;
    
    /**
     * list of types to be included in the app schema.
     */
    Collection<FeatureTypeInfo> types;

    /**
     * type mapping profile
     */
    TypeMappingProfile typeMappingProfile;
    
    public ApplicationSchemaXSD( NamespaceInfo ns, Catalog catalog, String baseURL, WFS wfs ) {
        this( ns, catalog, baseURL, wfs, Collections.EMPTY_LIST );
    }
    
    public ApplicationSchemaXSD( NamespaceInfo ns, Catalog catalog, String baseURL, WFS wfs, Collection<FeatureTypeInfo> types ) {
        this.ns = ns;
        this.catalog = catalog;
        this.baseURL = baseURL;
        this.wfs = wfs;
        this.types = types;
        
        if ( this.types == null ) {
            types = Collections.EMPTY_LIST;
        }
        
        if ( this.ns == null ) {
            //ns set to null, if all given types are from the same namespace, us that one
            if ( !types.isEmpty() ) {
                Iterator<FeatureTypeInfo> i = types.iterator();
                NamespaceInfo namespace = i.next().getNamespace();
                while( i.hasNext() ) {
                    FeatureTypeInfo type = i.next();
                    if ( !namespace.equals( type.getNamespace() ) ) {
                        namespace = null;
                        break;
                    }
                }
                
                if ( namespace != null ) {
                    this.ns = namespace;
                }
            }
        }
        
        //set up type mapping profiles
        Set<Schema> profiles = new LinkedHashSet<Schema>();
        for ( XSD xsd : wfs.getAllDependencies() ) {
            profiles.add( xsd.getTypeMappingProfile() );
        }
        typeMappingProfile = new TypeMappingProfile( profiles );
    }
    
    @Override
    protected void addDependencies(Set dependencies) {
        dependencies.add( wfs );
    }
    
    @Override
    public String getNamespaceURI() {
        //namespace will be null in case where types from different namespaces are 
        //included in this schema
        if ( ns != null ) {
            return ns.getURI();
        }
        return null;
    }

    @Override
    public String getSchemaLocation() {
        String schemaLocation = ResponseUtils.appendQueryString( ResponseUtils.appendPath( baseURL, "wfs" ), 
            "request=DescribeFeatureType&service=WFS&version=" + wfs.getVersion() );
        
        if ( types.isEmpty() ) {
            schemaLocation = ResponseUtils.appendQueryString( schemaLocation, "namespace=" + ns.getURI() );
        }
        else {
            StringBuffer sb = new StringBuffer( "typeName=");
            for ( FeatureTypeInfo type : types ) {
                sb.append( type.getPrefixedName() ).append( ",");
            }
            sb.setLength( sb.length() - 1);
            schemaLocation = ResponseUtils.appendQueryString( schemaLocation, sb.toString() );
        }
        
        return schemaLocation;
    }
    
    @Override
    protected XSDSchema buildSchema() throws IOException {
        XSDFactory factory = XSDFactory.eINSTANCE;
        XSDSchema schema = factory.createXSDSchema();
        
        schema.setSchemaForSchemaQNamePrefix("xsd");
        schema.getQNamePrefixToNamespaceMap().put("xsd", XSDConstants.SCHEMA_FOR_SCHEMA_URI_2001);
        schema.setElementFormDefault(XSDForm.get(XSDForm.QUALIFIED));

        if ( ns == null ) {
            //there must be types from different namespaces included, build a schema 
            // with imports
            if ( types.isEmpty() ) {
                //build for all namespaces
                buildSchemaImports( catalog.getNamespaces(), schema, factory );
            }
            else {
                Set<NamespaceInfo> namespaces = new HashSet<NamespaceInfo>();
                for ( FeatureTypeInfo type : types ) {
                    namespaces.add( type.getNamespace() );
                }
                buildSchemaImports( namespaces, schema, factory );
            }
        }
        else {
            schema.setTargetNamespace(ns.getURI());
            schema.getQNamePrefixToNamespaceMap().put(ns.getPrefix(), ns.getURI());
            
            schema.getQNamePrefixToNamespaceMap().put("wfs", WFS.NAMESPACE);
            schema.getQNamePrefixToNamespaceMap().put("gml", GML.NAMESPACE);
            
            //import wfs schema
            Schemas.importSchema( schema ,wfs.getSchema() );
            schema.resolveElementDeclaration( WFS.NAMESPACE, "FeatureCollection" );
            /*
            XSDImport imprt = factory.createXSDImport();
            imprt.setNamespace(WFS.NAMESPACE);
            
            String location = ResponseUtils.appendPath( baseURL, "schemas/wfs/");
            location += wfs.getVersion() + "/";
            location += new File( new URL(wfs.getSchemaLocation()).getFile() ).getName();
            imprt.setNamespace(location);
            imprt.setResolvedSchema(wfs.getSchema());
            schema.getContents().add(imprt);
            */
            
            Collection<FeatureTypeInfo> featureTypes = types;
            if ( featureTypes == null || featureTypes.isEmpty() ) {
                //grab all from catalog
                featureTypes = catalog.getFeatureTypesByNamespace(ns);
            }
            
            SchemaIndexImpl index = new SchemaIndexImpl( new XSDSchema[]{ schema });
            //all types in same namespace, write out the schema
            for (FeatureTypeInfo featureType : featureTypes ) {
                buildSchemaContent(featureType, schema, factory, index );
            }
        }
        
        return schema;
    }

    void buildSchemaImports(Collection<NamespaceInfo> namespaces, XSDSchema schema, XSDFactory factory) {
        
        Map<String, String> params = params("service", "wfs",
                "request", "DescribeFeatureType",
                "version", wfs.getVersion());
        
//        String schemaLocation = ResponseUtils.appendQueryString(ResponseUtils.appendPath( baseURL, "wfs"), 
//            "service=wfs&request=DescribeFeatureType&version=" + wfs.getVersion() + "&namespace=");
        
        for ( NamespaceInfo namespace : namespaces ) {
            XSDImport imprt = factory.createXSDImport();
            imprt.setNamespace(namespace.getURI());
            params.put("namespace", namespace.getPrefix());
            imprt.setSchemaLocation(buildURL(baseURL, "wfs", params, URLType.SERVICE));

            schema.getContents().add(imprt);
        }
    }
    
    void buildSchemaContent(FeatureTypeInfo featureTypeInfo,
            XSDSchema schema, XSDFactory factory, SchemaIndex index) throws IOException {

        //look if the schema for the type is already defined
        String prefix = featureTypeInfo.getNamespace().getPrefix();
        String store = featureTypeInfo.getStore().getName();
        String name = featureTypeInfo.getName();

        File schemaFile = null;
        try {
            schemaFile = catalog.getResourceLoader().find("workspaces" + "/" + prefix + "/" + store + 
                    "/" + name + "/schema.xsd");
        } catch (IOException e1) {
        }
       
        if (schemaFile != null) {
            //schema file found, parse it and lookup the complex type
            List locators = new ArrayList();
            for ( XSD xsd : wfs.getAllDependencies() ) {
                locators.add( xsd.createSchemaLocator() );
            }
            XSDSchema ftSchema = null;

            try {
                ftSchema = Schemas.parse(schemaFile.getAbsolutePath(), locators, null);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING,
                    "Unable to parse schema: " + schemaFile.getAbsolutePath(), e);
            }

            if (ftSchema != null) {
                //add the contents of this schema to the schema being built
                //look up the complex type
                List contents = ftSchema.getContents();

                for (Iterator i = contents.iterator(); i.hasNext();) {
                    XSDSchemaContent content = (XSDSchemaContent) i.next();
                    content.setElement(null);
                }

                schema.getContents().addAll(contents);
                schema.updateElement();

                return;
            }
        }
        
        //build the type manually
        SimpleFeatureType featureType = (SimpleFeatureType) featureTypeInfo.getFeatureType();
        
        XSDComplexTypeDefinition complexType = factory.createXSDComplexTypeDefinition();
        complexType.setName(name+ "Type");

        complexType.setDerivationMethod(XSDDerivationMethod.EXTENSION_LITERAL);
        complexType.setBaseTypeDefinition(
            schema.resolveComplexTypeDefinition(GML.NAMESPACE, "AbstractFeatureType"));

        XSDModelGroup group = factory.createXSDModelGroup();
        group.setCompositor(XSDCompositor.SEQUENCE_LITERAL);

        List attributes = featureType.getAttributeDescriptors();
        
        for (int i = 0; i < attributes.size(); i++) {
            AttributeDescriptor attribute = (AttributeDescriptor) attributes.get(i);
            if ( filterAttributeType( attribute ) ) {
                continue;
            }
            
           
            XSDElementDeclaration element = factory.createXSDElementDeclaration();
            element.setName(attribute.getLocalName());
            element.setNillable(attribute.isNillable());

            
            Class binding = attribute.getType().getBinding();
            Name typeName = findTypeName(binding);

            if (typeName == null) {
                throw new NullPointerException("Could not find a type for property: "
                    + attribute.getName() + " of type: " + binding.getName());
            }

            XSDTypeDefinition type = index.getTypeDefinition( new QName( typeName.getNamespaceURI(), typeName.getLocalPart() ) );
            if ( type == null ) {
                throw new IllegalStateException( "Could not find type: " + typeName );
            }
            //XSDTypeDefinition type = schema.resolveTypeDefinition(typeName.getNamespaceURI(),
            //        typeName.getLocalPart());
            element.setTypeDefinition(type);

            XSDParticle particle = factory.createXSDParticle();
            particle.setMinOccurs(attribute.getMinOccurs());
            particle.setMaxOccurs(attribute.getMaxOccurs());
            particle.setContent(element);
            group.getContents().add(particle);
        }

        XSDParticle particle = factory.createXSDParticle();
        particle.setContent(group);

        complexType.setContent(particle);

        schema.getContents().add(complexType);

        XSDElementDeclaration element = factory.createXSDElementDeclaration();
        element.setName(name);

        element.setSubstitutionGroupAffiliation(schema.resolveElementDeclaration(GML.NAMESPACE,
                "_Feature"));
        element.setTypeDefinition(complexType);

        schema.getContents().add(element);
        schema.updateElement();
    }
    
    boolean filterAttributeType( AttributeDescriptor attribute ) {
        return "name".equals( attribute.getName().getLocalPart() ) 
            || "description".equals( attribute.getName().getLocalPart() ) 
            || "boundedBy".equals( attribute.getName().getLocalPart());
    }
    
    Name findTypeName(Class binding) {
           return typeMappingProfile.name(binding);
    }
}
